﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Scribe4net
{

    public static class TestObjectSerializer
    {

        public static string SerializeToString(object obj, SerializationType serType = SerializationType.Xml, SerializationSettings settings = null)
        {
            if (settings == null)
                settings = new SerializationSettings();

            ISerializationWriter writer = null;

            //for a primitive object, just return a string.
            var objType = obj.GetType();
            if (IsPrimitiveType(objType))
            {
                return obj.ToString();
            }

            //for a complex object, create a writer and serialize by reflection
            switch (serType)
            {
                case SerializationType.Xml:
                    writer = new XmlSerializationWriter();
                    break;
                case SerializationType.XmlNoListTags:
                    writer = new XmlSerializationWriterNoListTags();
                    break;
                case SerializationType.Json:
                    writer = new JsonSerializationWriter();
                    break;
            }

            string objName = objType.Name;

            writer.Start(objName, (typeof(System.Collections.IEnumerable)).IsAssignableFrom(objType));

            //actual serializing here
            WriteToString(obj, 0, writer, settings);

            writer.End(objName);
            return writer.GetString();
        }

        private static bool IsPrimitiveType(Type propType)
        {
            return propType.IsPrimitive || propType.Equals(typeof(string)) || propType.Equals(typeof(DateTime));
        }

        private static void WriteToString(object originalObj, int dept, ISerializationWriter writer, SerializationSettings settings)
        {
            if (settings.MaxDept == int.MinValue)
                settings.MaxDept = -1;


            var objType = originalObj.GetType();
            if ((typeof(System.Collections.IDictionary)).IsAssignableFrom(objType))
            {
                WriteDictionary(dept, originalObj, writer, settings);
            }
            else if ((typeof(System.Collections.IEnumerable)).IsAssignableFrom(objType))
            {
                WriteList(dept, originalObj, writer, settings);
            }
            else
            {
                if (dept == settings.MaxDept)
                    return;

                WriteObject(originalObj, IncrementDepth(dept), writer, settings);
            }
        }

        private static void WriteObject(object originalObj, int dept, ISerializationWriter writer, SerializationSettings settings)
        {

            //++writer.IdentationLevel;
            string objTypeName = originalObj.GetType().Name;
            List<Tuple<string, Type, object>> infos = new List<Tuple<string, Type, object>>();

            //go over all properties
            PropertyInfo[] propInfos = originalObj.GetType().GetProperties(settings.PropertyInclusion);

            foreach (PropertyInfo info in propInfos)
            {
                var customAttribs = info.GetCustomAttributes(false);

                //if opting in, check for attribute before writing the property
                if (settings.OptIn && customAttribs.All(a => a.GetType() != typeof(TestSerializerOptInAttribute)))
                    continue;

                //if an opt out attribute exists, don't write property
                if (customAttribs.Any(a => a.GetType() == typeof(TestSerializerOptOutAttribute)))
                    continue;

                object objVal = info.GetValue(originalObj, null);
                infos.Add(new Tuple<string, Type, object>(info.Name, info.PropertyType, objVal));
            }

            if (settings.IncludeFields)
            {
                FieldInfo[] fieldInfos = originalObj.GetType().GetFields(settings.FieldInclusion);
                foreach (FieldInfo info in fieldInfos)
                {
                    var customAttribs = info.GetCustomAttributes(false);

                    //if opting in, check for attribute before writing the property
                    if (settings.OptIn && customAttribs.All(a => a.GetType() != typeof(TestSerializerOptInAttribute)))
                        continue;

                    //if an opt out attribute exists, don't write property
                    if (customAttribs.Any(a => a.GetType() == typeof(TestSerializerOptOutAttribute)))
                        continue;

                    object objVal = info.GetValue(originalObj);
                    infos.Add(new Tuple<string, Type, object>(info.Name, info.FieldType, objVal));
                }
            }

            writer.ObjectStart(objTypeName, dept, infos.Count() == 0);

            ++writer.IndentationLevel;
            foreach (var info in infos)
            {
                string name = info.Item1;
                Type propType = info.Item2;
                object objVal = info.Item3;

                if (objVal == null)
                {
                    writer.AppendNullValue(name, dept + 1, false);
                    continue;
                }

                //Type propType = info.PropertyType;
                bool isPrimitive = IsPrimitiveType(propType);
                writer.DictItemStart(name, dept, isPrimitive);

                if (isPrimitive)
                {
                    writer.AppendPrimitiveValue(objVal, dept + 1);
                }
                else
                {//for complex types check dept
                    {
                        ++writer.IndentationLevel;
                        WriteToString(objVal, dept, writer, settings);
                        --writer.IndentationLevel;
                    }
                }

                writer.DictItemEnd(name, dept);
            }
            --writer.IndentationLevel;

            writer.ObjectEnd(objTypeName, dept);
        }

        private static int IncrementDepth(int dept)
        {
            return dept == -1 ? dept : dept + 1;
        }

        private static void WriteList(int dept, object list, ISerializationWriter writer, SerializationSettings settings)
        {
            var enumerable = ((IEnumerable)list);
            int count = 0;
            foreach (object item in enumerable)
            {
                ++count;
            }

            writer.ListStart(enumerable.GetType().Name, dept, count == 0);

            if (dept < settings.MaxDept || settings.MaxDept == -1)
            {
                ++writer.IndentationLevel;
                foreach (object item in enumerable)
                {
                    if (item == null)
                    {
                        writer.AppendNullValue("ListItem", dept + 1, true);
                        continue;
                    }

                    Type propType = item.GetType();

                    bool isPrimitive = IsPrimitiveType(propType);
                    writer.ListItemStart(propType.Name, dept, isPrimitive);

                    if (isPrimitive)
                    {
                        writer.AppendPrimitiveValue(item, dept + 1);
                    }
                    else
                    {
                        WriteToString(item, dept, writer, settings);
                    }
                    writer.ListItemEnd(propType.Name, dept, isPrimitive);
                }
                --writer.IndentationLevel;
            }

            writer.ListEnd(enumerable.GetType().Name, dept);
        }

        private static void WriteDictionary(int dept, object dictionary, ISerializationWriter writer, SerializationSettings settings)
        {
            IDictionary dict = (IDictionary)dictionary;
            ICollection keys = ((IDictionary)dictionary).Keys;
            string dictTypeName = "Dictionary";
            writer.DictStart(dictTypeName, dept, dict.Count == 0);

            int counter = 1;
            if (dept < settings.MaxDept || settings.MaxDept == -1)
            {
                ++writer.IndentationLevel;
                foreach (object key in keys)
                {
                    string serKey = null;
                    //get value
                    object val = dict[key];
                    bool isPrimitiveVal = IsPrimitiveType(val.GetType());

                    //get key
                    if (IsPrimitiveType(key.GetType()))
                    {
                        serKey = key.ToString();
                    }
                    else
                    {
                        serKey = key.GetType().Name.Replace("`", "") + counter++;
                    }

                    writer.DictItemStart(serKey, dept, isPrimitiveVal);

                    if (isPrimitiveVal)
                    {
                        writer.AppendPrimitiveValue(val, dept + 1);
                    }
                    else
                    {
                        WriteToString(val, dept, writer, settings);
                    }

                    writer.DictItemEnd(serKey, dept);
                }
                --writer.IndentationLevel;
            }
            writer.DictEnd(dictTypeName, dept);
        }

    }
}
